
package.cpath = ";;./?.dll;./?.so;./lib/?.so;./lib/?.dll;"

require 'wx'
require 'lfs'
require 'lsqlite3'

-- Set up Icons

local projectTreeImages = wx.wxImageList(16,16);
local PROJTREE = {
	FOLDER = projectTreeImages:Add(wx.wxBitmap("folder.png"));
	FILE = projectTreeImages:Add(wx.wxBitmap("page_white.png"));
	PROJECT = projectTreeImages:Add(wx.wxBitmap("television.png"));
	SCRIPT = projectTreeImages:Add(wx.wxBitmap("page_white_lightning.png"));
	IMAGE = projectTreeImages:Add(wx.wxBitmap("picture.png"));
	FONT = projectTreeImages:Add(wx.wxBitmap("font.png"));
	MUSIC = projectTreeImages:Add(wx.wxBitmap("music.png"));
	DATABASE = projectTreeImages:Add(wx.wxBitmap("database.png"));
	DATABASEJOURNAL = projectTreeImages:Add(wx.wxBitmap("database_gear.png"));
	HOME = projectTreeImages:Add(wx.wxBitmap("house.png"));
	SEARCH = projectTreeImages:Add(wx.wxBitmap("zoom.png"));
}
local GREYED = setmetatable({}, {
	__index = function(me,name)
		local bmp_i = PROJTREE[name];
		if not bmp_i then
			return nil
		end
		local img = projectTreeImages:GetBitmap(bmp_i):ConvertToImage();
		local mr,mg,mb;
		if img:HasMask() then
			mr,mg,mb = img.MaskRed, img.MaskGreen, img.MaskBlue;
		end
		for x = 0,img.Width-1 do
			for y = 0,img.Height-1 do
				local r,g,b,a = img:GetRed(x,y), img:GetGreen(x,y), img:GetBlue(x,y), img:GetAlpha(x,y);
				if not (mr and r==mr and g==mg and b==mb) then
					local grey = 127 + math.floor((r+g+b)/3 * 0.5);
					img:SetRGB(x,y, grey,grey,grey);
					img:SetAlpha(x,y, a);
				end
			end
		end
		local greyed = projectTreeImages:Add(wx.wxBitmap(img));
		rawset(me, name, greyed);
		return greyed;
	end
});

-- STC Utility Functions

local char_CR  = string.byte("\r");
local char_LF  = string.byte("\n");
local char_OPEN_SQ = string.byte("[");

local font = wx.wxFont(10, wx.wxFONTFAMILY_TELETYPE, wx.wxFONTSTYLE_NORMAL, wx.wxFONTWEIGHT_NORMAL, false, "");

local function SetUpLuaSTC(stc)
	stc.Lexer = wxstc.wxSTC_LEX_LUA;
	
	stc.Font = font;
	stc:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, font);
	for i = 0, 32 do
		stc:StyleSetFont(i, font);
	end

	-- Whitespace
	stc:StyleSetForeground(0, wx.wxColour(128,128,128));
	-- Block comment
	stc:StyleSetForeground(1, wx.wxColour(0,127,0));
	-- Line comment
	stc:StyleSetForeground(2, wx.wxColour(0,127,0));
	-- ???
	stc:StyleSetForeground(3, wx.wxColour(127,127,127))
	-- Numbers
	stc:StyleSetForeground(4, wx.wxColour(0,150,175));
	-- Identifiers
	stc:StyleSetForeground(5, wx.wxColour(0,0,127));
	stc:StyleSetBold(5, true);
	-- Single quoted strings
	stc:StyleSetForeground(6, wx.wxColour(170,0,170));
	-- Double quoted strings
	stc:StyleSetForeground(7, wx.wxColour(170,0,170));
	-- Long strings
	stc:StyleSetForeground(8, wx.wxColour(170,0,170));
	-- Preprocessor (???)
	stc:StyleSetForeground(9, wx.wxColour(127,127,0));
	-- Operators
	stc:StyleSetForeground(10, wx.wxColour(0,0,0));
	stc:StyleSetBold(10, true);
	-- Identifiers (???)
	stc:StyleSetForeground(11, wx.wxColour(0,0,0));
	-- Unterminated strings
	stc:StyleSetForeground(12, wx.wxColour(0,0,0));
	stc:StyleSetBackground(12, wx.wxColour(224,192,224));
	stc:StyleSetBold(12, true);
	stc:StyleSetEOLFilled(12, true);
	-- Keyword 2 highlighting styles
	stc:StyleSetForeground(13, wx.wxColour(25,125,75));
	stc:StyleSetBold(13, true);
	-- Keyword 3
	stc:StyleSetForeground(14, wx.wxColour(25,75,175));
	stc:StyleSetBold(14, false);
	-- Keyword 4
	stc:StyleSetForeground(15, wx.wxColour(127,0,127));
	stc:StyleSetBold(15, true);
	-- Keyword 5
	stc:StyleSetForeground(16, wx.wxColour(127,127,0));
	stc:StyleSetBold(16, true);
	-- Keyword 6 
	stc:StyleSetForeground(17, wx.wxColour(35,95,175));
	stc:StyleSetBold(17, true);
	-- Keyword 7
	stc:StyleSetForeground(18, wx.wxColour(0,127,127));
	stc:StyleSetBold(18, true);
	-- Keyword 8
	stc:StyleSetBackground(18, wx.wxColour(240, 255, 255));
	stc:StyleSetForeground(19, wx.wxColour(0,   127, 127))
	stc:StyleSetBackground(19, wx.wxColour(224, 255, 255))
	stc:StyleSetForeground(20, wx.wxColour(0,   127, 127))
	stc:StyleSetBackground(20, wx.wxColour(192, 255, 255))
	stc:StyleSetForeground(21, wx.wxColour(0,   127, 127))
	stc:StyleSetBackground(21, wx.wxColour(176, 255, 255))
	stc:StyleSetForeground(22, wx.wxColour(0,   127, 127))
	stc:StyleSetBackground(22, wx.wxColour(160, 255, 255))
	stc:StyleSetForeground(23, wx.wxColour(0,   127, 127))
	stc:StyleSetBackground(23, wx.wxColour(144, 255, 255))
	stc:StyleSetForeground(24, wx.wxColour(0,   127, 127))
	stc:StyleSetBackground(24, wx.wxColour(128, 155, 255))
	-- Line numbers
	stc:StyleSetForeground(32, wx.wxColour(192, 192, 192));
	stc:StyleSetBackground(33, wx.wxColour(192, 192, 192)); 
	-- Brace match
	stc:StyleSetForeground(34, wx.wxColour(47,   47,   255))
	stc:StyleSetBold(34, true);                           
	-- Brace mismatch
	stc:StyleSetForeground(35, wx.wxColour(255,   47,   47))
	stc:StyleSetBold(35, true);
	-- Indentation guides
	stc:StyleSetForeground(36, wx.wxColour(192, 192, 192))
	stc:StyleSetBackground(37, wx.wxColour(255, 255, 255))
	
	stc.UseTabs = false;
	stc.TabWidth = 4;
	stc.Indent = 4;
	stc.IndentationGuides = true;

	stc:SetVisiblePolicy(wxstc.wxSTC_VISIBLE_SLOP, 3);

	stc:SetMarginWidth(0, stc:TextWidth(16, "99999")) -- line # margin

	stc:SetMarginWidth(1, 16) -- marker margin
	stc:SetMarginType(1, wxstc.wxSTC_MARGIN_SYMBOL)
	stc:SetMarginSensitive(1, true)

	stc:SetMarginWidth(2, 16) -- fold margin
	stc:SetMarginType(2, wxstc.wxSTC_MARGIN_SYMBOL)
	stc:SetMarginMask(2, wxstc.wxSTC_MASK_FOLDERS)
	stc:SetMarginSensitive(2, true)
	
	stc:SetFoldFlags(wxstc.wxSTC_FOLDFLAG_LINEBEFORE_CONTRACTED + wxstc.wxSTC_FOLDFLAG_LINEAFTER_CONTRACTED);

	local grey = wx.wxColour(128, 128, 128);
	stc:MarkerDefine(wxstc.wxSTC_MARKNUM_FOLDEROPEN,    wxstc.wxSTC_MARK_BOXMINUS, wx.wxWHITE, grey)
	stc:MarkerDefine(wxstc.wxSTC_MARKNUM_FOLDER,        wxstc.wxSTC_MARK_BOXPLUS,  wx.wxWHITE, grey)
	stc:MarkerDefine(wxstc.wxSTC_MARKNUM_FOLDERSUB,     wxstc.wxSTC_MARK_VLINE,    wx.wxWHITE, grey)
	stc:MarkerDefine(wxstc.wxSTC_MARKNUM_FOLDERTAIL,    wxstc.wxSTC_MARK_LCORNER,  wx.wxWHITE, grey)
	stc:MarkerDefine(wxstc.wxSTC_MARKNUM_FOLDEREND,     wxstc.wxSTC_MARK_BOXPLUSCONNECTED,  wx.wxWHITE, grey)
	stc:MarkerDefine(wxstc.wxSTC_MARKNUM_FOLDEROPENMID, wxstc.wxSTC_MARK_BOXMINUSCONNECTED, wx.wxWHITE, grey)
	stc:MarkerDefine(wxstc.wxSTC_MARKNUM_FOLDERMIDTAIL, wxstc.wxSTC_MARK_TCORNER,  wx.wxWHITE, grey)
	grey:delete()
	
	stc:SetKeyWords(0, [[
		and break do else elseif end false for function if in local
		nil not or repeat return then true until while continue
	]]);
	
	stc:Connect(wxstc.wxEVT_STC_MARGINCLICK,
		function(event)
			local line = stc:LineFromPosition(event.Position);
			if (event.Margin == 2) then
				if (bit.band(stc:GetFoldLevel(line), wxstc.wxSTC_FOLDLEVELHEADERFLAG) ~= 0) then
					stc:ToggleFold(line);
				end
			end
		end);
		
	stc:SetProperty("fold", "1");
	stc:SetProperty("fold.compact", "0");
	stc:SetProperty("fold.comment", "1");
	
	stc.CaretLineBackground = wx.wxColour(232,232,255);
	stc.CaretLineVisible = true;
	
	stc:Connect(wxstc.wxEVT_STC_CHARADDED,
		function(event)
			-- auto-indent
			local ch = event.Key;
			if (ch == char_LF) then
				local pos = stc.CurrentPos;
				local line = stc:LineFromPosition(pos);
				local indent = stc:GetLineIndentation(line-1);
				if (indent > 0) then
					stc:SetLineIndentation(line, indent);
					stc:GotoPos(pos + indent);
				end
			end
		end);
		
	local function DoFindMatchingBracePosition()
		local length = stc.Length;
		if (length == 0) then
			return -1,-1;
		end
		local pos = stc.CurrentPos;
		if (pos > 0) and (stc:PositionBefore(pos)==(pos-1)) then
			local prevc = stc:GetCharAt(pos-1);
			local prevs = stc:GetStyleAt(pos-1);
			if (prevs == 10) and ((prevc == 91) or (prevc == 93) or (prevc == 123)
			or (prevc == 125) or (prevc == 40) or (prevc == 41)) then
				return pos-1, stc:BraceMatch(pos-1);
			end
		end
		if (pos < length) and (stc:PositionAfter(pos)==(pos+1)) then
			local nextc = stc:GetCharAt(pos);
			local nexts = stc:GetStyleAt(pos);
			if (nexts == 10) and ((nextc == 91) or (nextc == 93) or (nextc == 123)
			or (nextc == 125) or (nextc == 40) or (nextc == 41)) then
				return pos, stc:BraceMatch(pos);
			end
		end
		return -1,-1;
	end
	
	stc:Connect(wxstc.wxEVT_STC_UPDATEUI,
		function(event)
			local braceAtCaret,braceOpposite = DoFindMatchingBracePosition();
			if (braceAtCaret > -1) and (braceOpposite == -1) then
				stc:BraceBadLight(braceAtCaret);
				stc:SetHighlightGuide(0);
			else
				stc:BraceHighlight(braceAtCaret, braceOpposite);
			end
			event:Skip();
		end);
	
	stc.ScrollWidth = stc:TextWidth(11, (" "):rep(80));
	
end

local STC_LOAD_BUFFER,STC_SAVE_BUFFER = 4096,4096;

local function LoadToSTC(stc, path)
	stc:ClearAll();
	local file = io.open(path, 'rb');
	while true do
		local chunk = file:read(STC_LOAD_BUFFER);
		if (chunk == nil) then  break;  end
		stc:AppendText(chunk);
	end
	file:close();
end

local function SaveFromSTC(stc, path)
	stc.ReadOnly = true;
	local file = io.open(path, 'wb');
	local len = stc.Length;
	for i = 0,len,STC_SAVE_BUFFER do
		file:write(stc:GetTextRange(i,i+STC_SAVE_BUFFER));
	end
	file:write(stc:GetTextRange(len%STC_SAVE_BUFFER,len));
	file:close();
	stc.ReadOnly = false;
end

local NEXT_ID = wx.wxID_HIGHEST;
local function nextID()
    NEXT_ID = NEXT_ID + 1;
    return NEXT_ID;
end

local function alert(message)
	wx.wxMessageBox(tostring(message));
end

local db = sqlite3.open("ide_settings.db3");

-- Initialise database
db:exec[=[

	CREATE TABLE IF NOT EXISTS recent_files (
		path
	);
	
	CREATE TABLE IF NOT EXISTS program_settings (
		name UNIQUE,
		value
	);
	
	CREATE TABLE IF NOT EXISTS recent_projects (
		path UNIQUE,
		loaded
	);

]=];

local program_settings = {}
for name,value in db:urows[[ SELECT name,value FROM program_settings ]] do
	program_settings[name] = value
end

local mainWindow = wx.wxFrame(
	wx.NULL,
	wx.wxID_ANY,
	"Avocadicide",
	wx.wxDefaultPosition,
	wx.wxSize(450, 450),
	wx.wxDEFAULT_FRAME_STYLE + wx.wxMAXIMIZE);

local titleBarSuffix = " - " .. mainWindow.Title;
	
local statusBar = mainWindow:CreateStatusBar(1) do
	mainWindow.StatusText = "Welcome to Avocadicide!";
end

local splitter = wx.wxSplitterWindow(mainWindow, wx.wxID_ANY) do
    splitter.MinimumPaneSize = 50; -- don't let it unsplit
    splitter.SashGravity = 0.05;
end

mainWindow.Icon = projectTreeImages:GetIcon(PROJTREE.PROJECT);

local extensions = {
	png = PROJTREE.IMAGE;
	jpg = PROJTREE.IMAGE;
	jpeg = PROJTREE.IMAGE;
	bmp = PROJTREE.IMAGE;
	
	xm = PROJTREE.MUSIC;
	mp3 = PROJTREE.MUSIC;
	ogg = PROJTREE.MUSIC;
	mod = PROJTREE.MUSIC;
	wav = PROJTREE.MUSIC;
	
	db3 = PROJTREE.DATABASE;
	["db3-journal"] = PROJTREE.DATABASEJOURNAL;
	
	lua = PROJTREE.SCRIPT;
	ttf = PROJTREE.FONT;
}

local function setWelcomePage(html)
	html:SetPage[[
		<html>
			<head>
				<title>Welcome Page</title>
			</head>
			<body>
		        <h3>Welcome to Avocadicide</h3>
			</body>
		</html>
	]];
end

local projectFileInfo = {};
local nodeIds = {};
local editornodes = {};

local addHtmlPage, addLuaEditPage;
local stcs = setmetatable({}, {__mode='kv'});
local notebookPageInfo = setmetatable({}, {__mode='k'});
local notebook = wx.wxNotebook(splitter, wx.wxID_ANY) do
	notebook.ImageList = projectTreeImages;
	function addHtmlPage(defaultTitle, icon)
		local page = wx.wxLuaHtmlWindow(notebook);
		
		notebookPageInfo[page:DynamicCast("wxWindow")] = {
			type = 'view_html';
			filePath = filePath;
			fileId = nil;
			getContents = function(self)
				return;
			end;
		};
		
		local pageWin = page:DynamicCast("wxWindow");
		function page:OnSetTitle(title)
			for i = 1,notebook.PageCount do
				if (pageWin == notebook:GetPage(i-1)) then
					notebook:SetPageText(i-1,title);
					break;
				end
			end
		end
		notebook:AddPage(page, defaultTitle, true, icon);
		return page;
	end
	
	local function deleteNotebookPage(tab)
		local delPage = notebook:GetPage(tab);
		local editornode = editornodes[delPage.Id];
		if editornode then
			editornode.editor = nil;
		end
		notebook:DeletePage(tab);
	end
	
	notebook:Connect(wx.wxEVT_MIDDLE_UP,
		function(event)
			local tab = notebook:HitTest(event.Position);
			if (tab == wx.wxNOT_FOUND) then  return;  end
			deleteNotebookPage(tab);
		end);
		
	local notebookTabContextMenu = wx.wxMenu{
		{wx.wxID_CLOSE, "Close me"},
		{wx.wxID_CLOSE_ALL, "Close all but me"}
	};
	
	local notebook_hit;
	
	notebook:Connect(wx.wxEVT_RIGHT_UP,
		function(event)
			notebook_hit = notebook:HitTest(event.Position);
			if (notebook_hit ~= wx.wxNOT_FOUND) then
				notebook:PopupMenu(notebookTabContextMenu);
			end
		end);
	
	notebook:Connect(wx.wxID_CLOSE, wx.wxEVT_COMMAND_MENU_SELECTED,
		function(event)
			deleteNotebookPage(notebook_hit);
		end);
	
	notebook:Connect(wx.wxID_CLOSE_ALL, wx.wxEVT_COMMAND_MENU_SELECTED,
		function(event)
			for i = 0,notebook_hit-1 do
				deleteNotebookPage(0);
			end
			for i = 2,notebook.PageCount do
				deleteNotebookPage(1);
			end
		end);
	
	notebook:Connect(wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGED,
		function(event)
			local selected = event.Selection;
			if (selected >= 0) then
				local page = notebook:GetPage(selected);
				local info = notebookPageInfo[page];
				if (info.filePath) then
					mainWindow.Title = info.filePath .. titleBarSuffix;
				else
					mainWindow.Title = notebook:GetPageText(selected) .. titleBarSuffix;
				end
				page:SetFocus();
				if stcs[page] then
					page:DynamicCast("wxStyledTextCtrl"):SetSTCFocus(true);
				end
			end
			event:Skip();
		end);
	
	function addLuaEditPage(filePath)
		if not (lfs.attributes(filePath,'mode') == 'file') then
			wx.wxMessageBox("File no longer exists");
			-- TODO: update gui now
			return nil
		end
		
		local fileName = filePath:match("[^\\/]*$");
		
		local editor = wxstc.wxStyledTextCtrl(notebook, wx.wxID_ANY, wx.wxDefaultPosition, notebook.ClientSize, wx.wxNO_BORDER);
		
		local editorWin = editor:DynamicCast("wxWindow");
		
		stcs[editorWin] = true;
		
		notebookPageInfo[editorWin] = {
			type = 'edit_lua';
			filePath = filePath;
			fileId = nil;
			modified = lfs.attributes(filePath, "modification");
		};
		
		SetUpLuaSTC(editor);
		
		editor:Connect(wxstc.wxEVT_STC_SAVEPOINTREACHED,
			function(event)
				for i = 0,notebook.PageCount-1 do
					if (notebook:GetPage(i) == editorWin) then
						notebook:SetPageImage(i, GREYED.SCRIPT);
						return;
					end
				end
			end);
				
		editor:Connect(wxstc.wxEVT_STC_SAVEPOINTLEFT,
			function(event)
				for i = 0,notebook.PageCount-1 do
					if (notebook:GetPage(i) == editorWin) then
						notebook:SetPageImage(i, PROJTREE.SCRIPT);
						return;
					end
				end
			end);
		
		editor.MouseDwellTime = 750;
		
		editor:Connect(wxstc.wxEVT_STC_DWELLSTART,
			function(event)
				local pos = event.Position;
				if (pos == -1) then  return;  end
				if (editor:GetStyleAt(pos) == 8) then
					local startpos = pos;
					
					-- Long string
					while (startpos > 0) and (editor:GetStyleAt(startpos) == 8) do
						startpos = startpos - 1;
					end
					
					if not ((editor:GetCharAt(startpos) == 70) and (editor:WordStartPosition(startpos,true) == startpos)) then
						return;
					end
					
					local endpos = pos;
					local length = editor.Length;
					
					while (endpos < length) and (editor:GetStyleAt(endpos) == 8) do
						endpos = endpos + 1;
					end
					
					local eqs = 0;
					while (editor:GetCharAt(startpos+eqs) == 61) do
						eqs = eqs + 1;
					end
					
					local overPath = editor:GetTextRange(startpos + 3 + eqs, endpos - 2 - eqs);
					
					alert(overPath);
				end
			end);
		
		LoadToSTC(editor, filePath);
		
		editor:EmptyUndoBuffer()
		
		notebook:AddPage(editor, fileName, true, GREYED.SCRIPT);
		
		return editor;
	end
	
	local welcomePage = addHtmlPage("Welcome!", PROJTREE.HOME);
	setWelcomePage(welcomePage);
end

local projectdb
local projectrootfolder

local searchBox, projectTree;
local function onSearch()
	local term = searchBox.Value;
	local resultsPage = addHtmlPage("\"" .. term .. "\"", PROJTREE.SEARCH);
	function resultsPage:OnLinkClicked(link)
		local test = link.Href:match("tree:(%d+)");
		if test then
			local nodeId = nodeIds[tonumber(test)];
			projectTree:EnsureVisible(nodeId);
			projectTree:SelectItem(nodeId);
			local event = wx.wxTreeEvent(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED);
			event.Item = nodeId;
			projectTree:ProcessEvent(event);
		end
	end
	resultsPage:AppendToPage([[
		<html>
			<body>
				<h3>Search: "]] .. term:gsub('[&<]', {["&"]='&amp;', ["<"]='&lt;'}) .. [["</h3>]]);
	--[[
	local searchCmd = assert(projectdb:prepare[=[
		SELECT * FROM project_file WHERE filename GLOB ?
	]=]);
	--]]
	local searchCmd = assert(projectdb:prepare[=[
		SELECT file.filename, file.tree_id, snippet(project_source_fts3) AS snippet FROM project_source_fts3 fts
		  INNER JOIN project_file file ON file.rowid = fts.file_row
		WHERE project_source_fts3 MATCH ?
	]=]);
	searchCmd:bind_values(term);
	local i = 1
	for result in searchCmd:nrows() do
		if (i > 1) then
			resultsPage:AppendToPage[[<hr>]];
		end
		resultsPage:AppendToPage(i .. ". <a href='tree:" .. result.tree_id .. "'>")
		resultsPage:AppendToPage(tostring(result.filename:gsub('[&<]', {["&"]='&amp;', ["<"]='&lt;'})));
		resultsPage:AppendToPage("</a>")
		resultsPage:AppendToPage("<pre>" .. tostring(result.snippet) .. "</pre>");
		i = i + 1
	end
	searchCmd:finalize();
	resultsPage:AppendToPage[[
			</body>
		</html>]];
end

local function initprojectdb()
	projectdb:exec[=[
		
		CREATE TABLE IF NOT EXISTS project_file (
			rel_path TEXT,
			filename TEXT,
			parent INTEGER,
			size INTEGER,
			last_modified DATETIME,
			tree_id INTEGER
		);
		
		CREATE VIRTUAL TABLE project_source_fts3 USING FTS3 (
			file_row INTEGER UNIQUE,
			contents TEXT,
			name TEXT
		);
		
		CREATE TRIGGER IF NOT EXISTS delete_project_file_fts3 AFTER delete ON project_file
		BEGIN
			DELETE FROM project_source_fts3 WHERE file_row = old.rowid;
		END;
		
		CREATE TABLE IF NOT EXISTS last_tabs (
			tab_type TEXT,
			file_row INTEGER,
			file_path TEXT,
			contents TEXT
		)
	
	]=];
end

local fileMenu -- defined later

local newProject do
	local newProjectDialog = wx.wxDialog(
		mainWindow,
		wx.wxID_ANY,
		"New Project");
	
	newProjectDialog.Icon = projectTreeImages:GetIcon(PROJTREE.PROJECT);
	
	newProjectDialog.Sizer = wx.wxBoxSizer(wx.wxVERTICAL);
	
	-- Title
	local newProjectNameLabel = wx.wxStaticText(newProjectDialog, wx.wxID_ANY, "Project Title:");
	local newProjectNameBox = wx.wxTextCtrl(newProjectDialog, wx.wxID_ANY);
	newProjectDialog.Sizer:Add(newProjectNameLabel, 0, wx.wxGROW + wx.wxLEFT + wx.wxRIGHT + wx.wxTOP, 5);
	newProjectDialog.Sizer:Add(newProjectNameBox, 0, wx.wxGROW + wx.wxLEFT + wx.wxRIGHT + wx.wxTOP, 5);
	
	-- Folder
	local newProjectFolderLabel = wx.wxStaticText(newProjectDialog, wx.wxID_ANY, "Create In Directory:");
	newProjectDialog.Sizer:Add(newProjectFolderLabel, 0, wx.wxGROW + wx.wxLEFT + wx.wxRIGHT + wx.wxTOP, 5);
	local newProjectFolderBox
	local newProjectFolderPanel = wx.wxPanel(newProjectDialog, wx.wxID_ANY) do
		newProjectFolderPanel.Sizer = wx.wxBoxSizer(wx.wxHORIZONTAL);
		
		newProjectFolderBox = wx.wxTextCtrl(newProjectFolderPanel, wx.wxID_ANY);
		newProjectFolderPanel.Sizer:Add(newProjectFolderBox, 1);
		
		local newProjectFolderButton = wx.wxButton(newProjectFolderPanel, wx.wxID_ANY, "...", wx.wxDefaultPosition, wx.wxDefaultSize,
		wx.wxBU_EXACTFIT);
		newProjectFolderButton:Connect(wx.wxEVT_COMMAND_BUTTON_CLICKED,
			function()
				local dirSelect = wx.wxDirDialog(
					newProjectDialog,
					"Please Select A Project Home Directory",
					"");
				if lfs.attributes(newProjectFolderBox.Value,'mode') == 'directory' then
					dirSelect.Path = newProjectFolderBox.Value;
				else
					dirSelect.Path = lfs.currentdir();
				end
				if (dirSelect:ShowModal() == wx.wxID_OK) then
					newProjectFolderBox.Value = dirSelect.Path;
				end
			end);
		newProjectFolderPanel.Sizer:Add(newProjectFolderButton, 0);
	end
	newProjectDialog.Sizer:Add(newProjectFolderPanel, 0, wx.wxGROW + wx.wxLEFT + wx.wxRIGHT, 5);
	
	--[[
	local newProjectCreateFolderCheckbox =
		wx.wxCheckbox(newProjectDialog, wx.wxID_ANY, "Create Project Folder?", wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxALIGN_RIGHT);
	newProjectDialog.Sizer:Add(newProjectCreateFolderCheckbox, 0, wx.wxGROW + wx.wxLEFT + wxRIGHT, 5);
	--]]
	
	local newProjectButtonsPanel = wx.wxPanel(newProjectDialog, wx.wxID_ANY) do
		newProjectButtonsPanel.Sizer = wx.wxBoxSizer(wx.wxHORIZONTAL);
		newProjectButtonsPanel.Sizer:AddStretchSpacer();
		
		local newProjectCreateButton = wx.wxButton(newProjectButtonsPanel, wx.wxID_ANY, "Create");
		newProjectCreateButton:Connect(wx.wxEVT_COMMAND_BUTTON_CLICKED,
			function()
				if (newProjectNameBox.Value == "") then
					wx.wxMessageBox("You must choose a name for the project.");
					newProjectNameBox:SetFocus();
				elseif (lfs.attributes(newProjectFolderBox.Value,"mode") ~= 'directory') then
					wx.wxMessageBox("You must choose a valid existing path for the project.");
					newProjectFolderBox:SetFocus();
				else
					newProjectDialog:EndModal(wx.wxID_OK);
				end
			end);
		newProjectButtonsPanel.Sizer:Add(newProjectCreateButton, 0, wx.wxRIGHT, 5);
			
		local newProjectCancelButton = wx.wxButton(newProjectButtonsPanel, wx.wxID_ANY, "Cancel");
		newProjectCancelButton:Connect(wx.wxEVT_COMMAND_BUTTON_CLICKED,
			function()
				newProjectDialog:EndModal(wx.wxID_CANCEL);
			end);
		newProjectButtonsPanel.Sizer:Add(newProjectCancelButton, 0, wx.wxRIGHT, 5);
		
		newProjectButtonsPanel.Sizer:AddStretchSpacer();
	end
	newProjectDialog.Sizer:AddStretchSpacer();
	newProjectDialog.Sizer:Add(newProjectButtonsPanel, 0, wx.wxGROW + wx.wxLEFT + wx.wxBOTTOM, 5);

	function newProject()
		if (newProjectDialog:ShowModal() == wx.wxID_OK) then
			
			alert(newProjectNameBox.Value);
		end
	end
end

local function closeProject()
	if not projectdb then  return;  end
	projectdb:exec[[ BEGIN ]];
	projectdb:exec[[ DELETE FROM last_tabs ]];
	local tabInsertCmd = projectdb:prepare[[ INSERT INTO last_tabs (tab_type,file_row,file_path,contents) VALUES (?,?,?,?) ]];
	for i = 0,notebook.PageCount-1 do
		local page = notebook:GetPage(i);
		local info = notebookPageInfo[page:DynamicCast("wxWindow")];
		if info then
			local contents = info.getContents and info.getContents(page);
			tabInsertCmd:bind_values(info.type, info.fileId, info.filePath, contents);
			tabInsertCmd:step();
			tabInsertCmd:reset();
		end
	end
	tabInsertCmd:finalize();
	projectdb:exec[[ COMMIT ]];
	projectdb:close()
	projectdb = nil
	
	projectrootfolder = nil;
	projectFileInfo = {};
	nodeIds = {};
	projectTree:DeleteAllItems();
	
	collectgarbage()
	
	fileMenu:Enable(wx.wxID_CLOSE, false);
end

local function openProject(path)
	closeProject();
	projectTree:DeleteAllItems();

	local parentdir, project = path:match("^(.-)[\\/]([^\\/]*)$");
	local projectname = project:sub(1, project:lower():match("()%.project%.lua$")-1);
	projectrootfolder = parentdir:lower():gsub('\\','/') .. '/';
	
	projectdb = sqlite3.open(projectrootfolder .. projectname .. '.ide.db3');
	
	projectdb:exec[[ BEGIN ]];
	
	initprojectdb();
	
	projectdb:exec[[ UPDATE project_file SET tree_id = NULL ]];
	
	local setLastProjectCmd = db:prepare[[ INSERT OR REPLACE INTO recent_projects (path,loaded) VALUES (?,DATETIME('now')) ]] do
		setLastProjectCmd:bind_values(path);
		setLastProjectCmd:step();
		setLastProjectCmd:finalize();
	end
	
	local insertCmd = projectdb:prepare[=[
		INSERT INTO project_file (rel_path, filename, parent, size, last_modified, tree_id) VALUES (?,?,?,?,?,?);
	]=];
	
	local addFullTextCmd = projectdb:prepare[=[
		INSERT INTO project_source_fts3 (file_row,contents,name) VALUES (?,?,?);
	]=] or error(projectdb:errmsg());
	
	local rootId = projectTree:AddRoot(projectname, PROJTREE.PROJECT);
	
	projectFileInfo = {[rootId.Value] = {rowid=nil, path=parentdir, type='<project>'}};
	nodeIds = {[rootId.Value] = rootId};
	
	local recurseDir;
	function recurseDir(folder, parentNodeId, parentRowId)
		projectTree.ImageList = projectTreeImages;
		local lastFolderId;
		for path in lfs.dir(projectrootfolder .. folder) do
			
			if not (path=='.' or path=='..') then
				local attrs = lfs.attributes(projectrootfolder .. folder .. path);
				
				local treeId;
				if (attrs.mode == "directory") then
					if lastFolderId then
						treeId = projectTree:InsertItem(parentNodeId, lastFolderId, path);
					else
						treeId = projectTree:PrependItem(parentNodeId, path);
					end
					lastFolderId = treeId;
				else
					treeId = projectTree:AppendItem(parentNodeId, path);
				end
				nodeIds[treeId.Value] = treeId;
				
				insertCmd:bind_values(
					folder .. path:lower(),
					path,
					parentRowId,
					(attrs["mode"] ~= 'directory') and attrs.size or nil,
					os.date('%Y-%m-%d %H:%M:%S',attrs.modification),
					treeId.Value
				);
				insertCmd:step();
				local rowid = projectdb:last_insert_rowid();
				insertCmd:reset();
				
				if (attrs["mode"] == 'directory') then
					recurseDir(folder..path:lower()..'/', treeId, rowid);
					projectTree:SetItemImage(treeId, PROJTREE.FOLDER);
					projectFileInfo[treeId.Value] = {
						rowid=rowid, rel_path=folder..path, path=projectrootfolder..folder..path, type='<folder>', icon=PROJTREE.FOLDER
					};
				else
					local extension = path:match("[^%.]+$");
					extension = extension and extension:lower();
					local filetype = extension and extensions[extension];
					filetype = filetype or PROJTREE.FILE;
					projectTree:SetItemImage(treeId, filetype);
					projectFileInfo[treeId.Value] = {
						rowid=rowid, rel_path=folder..path, path=projectrootfolder..folder..path, type=extension or '<file>', icon = filetype
					};
					if (extension == 'lua') then
						local file = io.open(projectrootfolder..folder..path, 'rb')
						local contents = file:read('*a')
						file:close()
						addFullTextCmd:bind_values(rowid, contents, path)
						addFullTextCmd:step()
						addFullTextCmd:reset()
					end
				end
			end
			
		end
	end
	
	recurseDir('', rootId);
	projectTree:Expand(rootId);
	
	insertCmd:finalize();
	addFullTextCmd:finalize();
	
	projectdb:exec[[ DELETE FROM project_file WHERE tree_id IS NULL ]]
	
	projectdb:exec[[ COMMIT ]];	
	
	fileMenu:Enable(wx.wxID_CLOSE, true);
end


local imagePreviewFrame = wx.wxMiniFrame(
	mainWindow,
	wx.wxID_ANY,
	"Popup Image",
	wx.wxDefaultPosition,
	wx.wxSize(300,300),
	wx.wxBORDER_NONE + wx.wxFRAME_FLOAT_ON_PARENT);
imagePreviewFrame:Connect(wx.wxEVT_MOTION,
	function(event)
		imagePreviewFrame:Hide();
	end);
local imagePreviewItemId = nil;
local imagePreviewBitmap = nil;
imagePreviewFrame:Connect(wx.wxEVT_PAINT,
	function(event)
		local dc = wx.wxPaintDC(imagePreviewFrame);
		if imagePreviewBitmap then
			dc.Pen = wx.wxBLACK_PEN;
			dc.Brush = wx.wxTRANSPARENT_BRUSH;
			dc:DrawRectangle(0,0,imagePreviewBitmap.Width+2,imagePreviewBitmap.Height+2);
			dc:DrawBitmap(imagePreviewBitmap, 1, 1, false);
		end
	end);

local ID_RENAME = nextID();
	
local leftPanel = wx.wxPanel(splitter, wx.wxID_ANY) do
	leftPanel.Sizer = wx.wxBoxSizer(wx.wxVERTICAL);
	
	local searchPanel = wx.wxPanel(leftPanel, wx.wxID_ANY) do
		searchPanel.Sizer = wx.wxBoxSizer(wx.wxHORIZONTAL);
		
		searchBox = wx.wxTextCtrl(searchPanel, wx.wxID_ANY) do
		end
		searchPanel.Sizer:Add(searchBox, 1, wx.wxGROW + wx.wxRIGHT, 0);
		
		local searchButton = wx.wxButton(searchPanel, wx.wxID_ANY, "Search", wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxBU_EXACTFIT) do
			searchButton.ToolTip = wx.wxToolTip("Search");
			searchButton:Connect(wx.wxEVT_COMMAND_BUTTON_CLICKED, onSearch);
		end
		searchPanel.Sizer:Add(searchButton, 0, wx.wxALIGN_RIGHT, 0);
	end
	leftPanel.Sizer:Add(searchPanel, 0, wx.wxGROW + wx.wxRIGHT, 0);
	
	projectTree = wx.wxTreeCtrl(leftPanel, wx.wxID_ANY, wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxTR_NO_BUTTONS + wx.wxTR_EDIT_LABELS) do
	
		local imagePreviewTimer = wx.wxTimer(projectTree);
		projectTree:Connect(wx.wxEVT_TIMER,
			function()
				local mousePos = wx.wxGetMousePosition();
				local itemId, flags = projectTree:HitTest(projectTree:ScreenToClient(mousePos));
				item = projectFileInfo[itemId.Value];
				if item and (bit.band(flags, wx.wxTREE_HITTEST_ONITEMLABEL + wx.wxTREE_HITTEST_ONITEMICON) > 0) then
					imagePreviewItemId = itemId.Value;
					if item.type == 'png' then
						imagePreviewBitmap = wx.wxBitmap(item.path);
						local rect = wx.wxRect();
						projectTree:GetBoundingRect(itemId,rect,true);
						local point = wx.wxPoint(rect.Right, rect.Top);
						point = projectTree:ClientToScreen(point);
						imagePreviewFrame:SetSize(point.X, point.Y, imagePreviewBitmap.Width + 2, imagePreviewBitmap.Height + 2);
--						imagePreviewFrame:SetSize(mousePos.X + 2, mousePos.Y + 2, imagePreviewBitmap.Width + 2, imagePreviewBitmap.Height + 2);
						imagePreviewFrame:Show();
					end
				end
			end);
		
		projectTree:Connect(wx.wxEVT_COMMAND_TREE_BEGIN_DRAG,
			function(event)
				event:Allow();
			end);
		
		projectTree:Connect(wx.wxEVT_COMMAND_TREE_END_DRAG,
			function(event)
				alert(event.Item.Value);
			end);
		
		local projectTreeContextMenu = wx.wxMenu{
			{ID_RENAME,			"&Rename\tF2",	"Choose a different name for this item."},
			{wx.wxID_DELETE,	"&Delete",		"Delete this item from the Project."}
		};
		
		local treeItemClicked;
		
		projectTree:Connect(wx.wxEVT_COMMAND_TREE_ITEM_MENU,
			function(event)
				if imagePreviewFrame:IsShown() then
					imagePreviewFrame:Hide();
				end
				if imagePreviewTimer:IsRunning() then
					imagePreviewTimer:Stop();
				end
				treeItemClicked = event.Item;
				projectTree:PopupMenu(projectTreeContextMenu);
			end);
			
		projectTree:Connect(ID_RENAME, wx.wxEVT_COMMAND_MENU_SELECTED,
			function(event)
				projectTree:EditLabel(treeItemClicked);
			end);
		
		projectTree:Connect(wx.wxEVT_COMMAND_TREE_KEY_DOWN,
			function(event)
				if (event.KeyCode == wx.WXK_F2) then
					projectTree:EditLabel(projectTree.Selection);
				end
			end);
		
		projectTree:Connect(wx.wxEVT_COMMAND_TREE_END_LABEL_EDIT,
			function(event)
				local newLabel = event.Label;
				if (newLabel == '') then
					event:Veto();
					return;
				end
				if newLabel:match('[:/\\%?%*<>"%|]') then
					wx.wxMessageBox("Cannot rename: Invalid character(s).");
					event:Veto();
					return;
				end
				local parent = projectTree:GetItemParent(event.Item);
				local parentnode = projectFileInfo[parent.Value];
				local existing = lfs.attributes(parentnode.path .. '/' .. newLabel, 'mode');
				if existing then
					wx.wxMessageBox("Cannot rename: A " .. existing .. " with this name already exists here.");
					event:Veto();
					return;
				end
			end);
	
		projectTree:Connect(wx.wxEVT_MOTION,
			function(event)
				if imagePreviewFrame:IsShown() then
					local itemId, flags = projectTree:HitTest(event.Position);
					if imagePreviewItemId == itemId.Value then  return;  end
				end
				imagePreviewFrame:Hide();
				local ms = wx.wxGetMouseState();
				if not (ms:LeftDown() or ms:RightDown() or ms:MiddleDown()) then
					imagePreviewTimer:Start(750, true);
				end
			end);
	
		projectTree:Connect(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED,
			function(event)
				local node = projectFileInfo[event.Item.Value];
				local editor = node.editor
				if editor then
					editor = editor:DynamicCast("wxWindow");
					for i=0,notebook.PageCount-1 do
						if (notebook:GetPage(i) == editor) then
							notebook.Selection = i;
							return;
						end
					end
				end
				if (node.type == 'lua') then
					local editor = addLuaEditPage(node.path);
					node.editor = editor;
					editornodes[editor.Id] = node;
				end
			end);
		
	end
	leftPanel.Sizer:Add(projectTree, 3, wx.wxGROW + wx.wxRIGHT + wx.wxBOTTOM, 0);
end

local ID_NEWPROJECT = nextID();

local menuBar = wx.wxMenuBar() do

	fileMenu = wx.wxMenu{
		{ID_NEWPROJECT,	"&New Project...",	"Create a new Guacamole Project"},
		{wx.wxID_OPEN,	"&Open Project...",	"Open a Guacamole Project"},
		{wx.wxID_CLOSE,	"&Close Project",	"Stop working on the current project"},
		{},
		{wx.wxID_SAVE,	"&Save\tCtrl-S",	"Save the currently opened file"},
		{},
		{wx.wxID_EXIT,	"E&xit",			"Close Avocadicide"}
	};
	fileMenu:Enable(wx.wxID_CLOSE, false);
	
	menuBar:Append(fileMenu, "&File");
	
	mainWindow:Connect(ID_NEWPROJECT, wx.wxEVT_COMMAND_MENU_SELECTED, newProject);
	
	mainWindow:Connect(wx.wxID_SAVE, wx.wxEVT_COMMAND_MENU_SELECTED,
		function(event)
			local info = notebookPageInfo[notebook.CurrentPage];
			if info then
				if (info.type == 'edit_lua') then
					local editor = notebook.CurrentPage:DynamicCast("wxStyledTextCtrl");
					SaveFromSTC(editor, info.filePath);
					info.modified = lfs.attributes(info.filePath, 'modification');
					editor:EmptyUndoBuffer();
					editor:SetSavePoint();
				end
			end
		end);
	
	mainWindow:Connect(wx.wxEVT_MENU_OPEN,
		function(event)
			local page = notebook.CurrentPage;
			local info = notebookPageInfo[page];
			fileMenu:Enable(wx.wxID_SAVE, info and (info.type == "edit_lua") and page:DynamicCast("wxStyledTextCtrl").Modify);
			event:Skip();
		end);
	
	mainWindow:Connect(wx.wxID_OPEN, wx.wxEVT_COMMAND_MENU_SELECTED,
		function()
			local openDialog = wx.wxFileDialog(mainWindow,
				"Choose a Project", "", "",
				"Guacamole Projects (*.project.lua)|*.project.lua",
				wx.wxOPEN + wx.wxFILE_MUST_EXIST);
			if (openDialog:ShowModal() == wx.wxID_CANCEL) then
				return;
			end
			openProject(openDialog.Path);
		end);
		
	mainWindow:Connect(wx.wxID_CLOSE, wx.wxEVT_COMMAND_MENU_SELECTED, closeProject);
		
	mainWindow:Connect(wx.wxID_EXIT, wx.wxEVT_COMMAND_MENU_SELECTED,
		function()
			mainWindow:Close(true);
		end);
	
    local helpMenu = wx.wxMenu() do
	
	    helpMenu:Append(wx.wxID_ABOUT, "&About", "About the Avocadicide application")
		mainWindow:Connect(wx.wxID_ABOUT, wx.wxEVT_COMMAND_MENU_SELECTED,
			function()
	            wx.wxMessageBox(
					table.concat({
						'Avocadicide - Guacamole IDE',
						'',
	                    'Made with '..wxlua.wxLUA_VERSION_STRING..' / '..wx.wxVERSION_STRING
					},'\n'),
					"About Avocadicide",
					wx.wxOK + wx.wxICON_INFORMATION,
					mainWindow
				);
			end
		);
		
		menuBar:Append(helpMenu, "&Help");
	end
	
	mainWindow.MenuBar = menuBar;
	
end

splitter:SplitVertically(leftPanel, notebook, 200);

for projectpath in db:urows[[ SELECT path FROM recent_projects ORDER BY loaded DESC ]] do
	openProject(projectpath);
	break;
end

mainWindow:Connect(wx.wxEVT_ACTIVATE,
	function(event)
		if not event.Active then
			event:Skip();
			return;
		end
		if getmetatable(notebook) == nil then
			event:Skip();
			return;
		end
		for i=0,notebook.PageCount-1 do
			local page = notebook:GetPage(i);
			local info = notebookPageInfo[page];
			local ignoreModified = info.ignoreModified;
			if info.filePath and info.modified and (info.modified ~= info.ignoreModified)
					and (lfs.attributes(info.filePath, 'modification') ~= info.modified) then
				info.ignoreModified = info.modified;
				if (wx.wxMessageBox(info.filePath .. " has been modified, Would you like to reload it?",
						"Externally Modified File", wx.wxYES_NO, mainWindow) == wx.wxYES) then
					local file = io.open(info.filePath, 'rb');
					local editor = page:DynamicCast("wxStyledTextCtrl");
					editor.Text = file:read("*a");
					file:close();
					editor:EmptyUndoBuffer();
					editor:SetSavePoint();
					info.modified = lfs.attributes(info.filePath, 'modification');
				end
			end
		end
		event:Skip();
	end);

mainWindow:Connect(wx.wxEVT_CLOSE_WINDOW,
	function(event)
		local success,er = pcall(closeProject);
		if not success then
			wx.wxMessageBox(er);
		end
		event:Skip()
	end);

mainWindow:Show(true);

wx.wxGetApp():MainLoop();
